import type { AppointmentManagement } from '@shared/types/'
import { normalizeAppointmentTime } from '@shared/utils/appointment-time'
import * as cheerio from 'cheerio'
import { getConfig } from '../config/index'
import { createContextLogger, generateCorrelationId } from '../logger/index'
import { LogCategory } from '../logger/types'
import { HttpSession } from '../request/http-session'
import { HttpSessionV2 } from '../request/http-session-v2'
import { selectProxyForRequestSession } from '../request/proxy-utils'
import { TdabsFlows } from '../request/tdabs-flows'
import { getAppointmentService } from '../services/appointment-service'
import { getMonitoringBookingTaskService } from '../services/monitoring-booking-task-service'
import { getReleaseRecordService } from '../services/release-record-service'

import { getPageParser } from '../shared/page-parser'
import { BookingEngineForBooking } from './booking-engine-booking'
import { getUnifiedBookingService } from './booking-service'
import { analyzeCalendarSummary, buildMonthsRange, gotoMonth as gotoCalMonth, parseDDMMYYYY, readCalendar as readCal } from './helpers/calendar'
import { generateRandomRelease } from './helpers/mock-release'
import { getMonitoringTaskExecutor } from './monitoring-task-executor'
import * as limiter from './session-limiter'

/** 监控启动参数 */
export interface MonitorStartParams {
  monitor: AppointmentManagement
  candidates: AppointmentManagement[]
  concurrency: number
  intervalMs?: number
  windowDays?: number
  options?: { mode?: 'playwright' | 'request', headless?: boolean, timeoutMs?: number, monitorOnly?: boolean, allowedDateRange?: { start?: string, end?: string }, debug?: { mockAvailableDates?: string[], forceFound?: boolean } }
}

/** 预约监控实例 */
class BookingAvailabilityMonitor {
  /** 是否运行中 */
  private running = false
  /** 日志记录器 */
  private logger = createContextLogger({ component: 'BookingMonitor', category: LogCategory.MONITOR })
  /** 浏览器引擎 */
  private engine: BookingEngineForBooking | null = null
  // Request 模式相关（使用 HttpSessionV2）
  private flows: TdabsFlows | null = null
  /** HttpSessionV2 实例 */
  private session: any = null
  /** Request 模式：当前日历状态（用于复用会话，避免每次都重新初始化） */
  private currentCalendarState: {
    html: string
    formFields: Record<string, string>
    year: number
    month0: number
  } | null = null

  /** 初始化阶段重试计数（用于指数回退） */
  private initRetryCount = 0
  /** 监控探测使用的随机身份（避免同账号重复登录） */
  private monitorIdentity: { passport: string, license: string, name: string, probeId: string } | null = null

  /** 报告器 */
  private reporter: ((update: { phase?: 'info' | 'warn' | 'success' | 'error', step?: string, message?: string, progress?: number, timestamp?: number, extra?: any }) => void) | null = null
  /** 中断控制器 */
  private abortController: AbortController | null = null
  /** 若已停止则抛出，用于在长链路中快速中断后续逻辑 */
  private ensureRunning(): void {
    if (!this.running)
      throw new Error('monitor stopped')
  }

  /**
   * 可中断延迟
   *
   * @description 结合 abortController.signal，一旦 stop() 被调用立即抛出
   *
   * @param {number} ms - 延迟时间（毫秒）
   * @returns {Promise<void>} 无返回值
   */
  private delayCancellable(ms: number): Promise<void> {
    const signal = this.abortController?.signal
    if (!signal)
      return new Promise(resolve => setTimeout(resolve, ms))
    return new Promise((resolve, reject) => {
      let timer: any
      const cleanup = () => {
        try { clearTimeout(timer) }
        catch {}
      }
      const onAbort = () => { cleanup(); reject(new Error('monitor aborted')) }
      timer = setTimeout(() => { cleanup(); resolve() }, ms)
      try { signal.addEventListener('abort', onAbort as any, { once: true } as any) }
      catch {}
    })
  }

  /**
   * 初始化阶段指数回退（aborted/timeout/解析失败/512-E-0044）
   */
  private computeInitBackoffMs(error: string, retryCount: number): number {
    const e = String(error || '').toLowerCase()
    const isDupLogin = /512-e-0044|duplicate_login|重[複覆]登入|系统而中断|系統而中斷/.test(e)
    const isTimeout = /connect timeout|etimedout|timeout/.test(e)
    const isAborted = /this operation was aborted|aborted/.test(e)
    const isParseFailed = /parse_failed|无法进入日历页|無法進入日曆頁|无法解析日历页/.test(e)

    let base = 0
    let max = 0
    if (isDupLogin) { base = 5000; max = 30000 }
    else if (isTimeout || isAborted) { base = 700; max = 10000 }
    else if (isParseFailed) { base = 500; max = 5000 }
    else { return 0 }

    const exp = Math.max(1, Math.min(5, retryCount)) - 1
    const jitter = Math.floor(100 + Math.random() * 300)
    return Math.min(max, Math.floor(base * (2 ** exp)) + jitter)
  }

  /**
   * 是否运行中
   *
   * @description 检查监控是否正在运行
   *
   * @returns {boolean} 是否运行中
   */
  isRunning(): boolean { return this.running }
  /**
   * 设置报告器
   *
   * @description 设置监控报告器
   *
   * @returns {void} 无返回值
   */
  setReporter(fn: ((update: { phase?: 'info' | 'warn' | 'success' | 'error', step?: string, message?: string, progress?: number, timestamp?: number, extra?: any }) => void) | null): void {
    this.reporter = fn
  }

  /**
   * 报告
   *
   * @description 报告监控状态
   *
   * @returns {void} 无返回值
   */
  private report(update: { phase?: 'info' | 'warn' | 'success' | 'error', step?: string, message?: string, progress?: number, extra?: any }) {
    try { this.reporter?.({ ...update, timestamp: Math.floor(Date.now() / 1000) }) }
    catch {}
  }

  /**
   * 启动监控
   *
   * @description 启动监控任务
   *
   * @param {MonitorStartParams} params - 监控启动参数
   * @returns {Promise<void>} 无返回值
   */
  async start(params: MonitorStartParams): Promise<void> {
    if (this.running)
      return
    this.running = true
    this.abortController = new AbortController()
    this.report({ phase: 'info', step: '启动监控', message: '监控已启动', progress: 0 })

    // 启动任务执行器（如果未启动）
    try {
      const executor = getMonitoringTaskExecutor()
      if (!executor.getStatus().running) {
        await executor.start()
        this.logger.info('监控任务执行器已启动')
      }
    }
    catch (error: any) {
      this.logger.warn('启动任务执行器失败', { error: error?.message || String(error) })
    }

    const _cfg = getConfig() as any
    const interval = Math.max(10_000, Number(params.intervalMs || 30_000))
    const windowDays = Math.max(1, Number(params.windowDays || _cfg?.booking?.available_window_days || 70))

    // 监控阶段：限流为1，避免占满资源
    try { limiter.setLimit(1) }
    catch {}

    // 监控循环
    while (this.running) {
      try {
        const correlationId = generateCorrelationId()
        this.report({ phase: 'info', step: '监控探测', message: '开始一次探测', progress: 10 })
        await this.runSingleProbe(params, windowDays, correlationId)
        this.report({ phase: 'info', step: '监控探测完成', message: '一次探测完成', progress: 20 })
      }
      catch (e) {
        this.logger.warn('监控探测异常', { error: (e as any)?.message || String(e) })
        this.report({ phase: 'warn', step: '监控探测异常', message: (e as any)?.message || String(e) })
      }

      if (!this.running)
        break
      try {
        await this.delayCancellable(interval)
      }
      catch {
        break
      }
    }
  }

  /**
   * 停止监控
   *
   * @description 停止监控任务
   *
   * @returns {Promise<void>} 无返回值
   */
  async stop(): Promise<void> {
    this.running = false
    try { this.abortController?.abort() }
    catch {}
    try { await (this.engine as any)?.browser?.close?.() }
    catch {}
    this.engine = null
    // 清理 Request 模式资源
    this.flows = null
    this.session = null
    this.currentCalendarState = null

    // 注意：不停止任务执行器，因为它可能被其他监控使用
    // 如果需要停止，可以在应用关闭时统一停止

    this.report({ phase: 'warn', step: '停止监控', message: '监控已停止', progress: 100 })
  }

  /**
   * 运行单次探测
   *
   * @description 运行单次探测任务
   *
   * @param {MonitorStartParams} params - 探测启动参数
   * @param {number} windowDays - 探测窗口天数
   * @returns {Promise<void>} 无返回值
   */
  private async runSingleProbe(params: MonitorStartParams, windowDays: number, correlationId?: string): Promise<void> {
    this.ensureRunning()
    const mode = params.options?.mode || 'playwright'

    // 根据模式选择执行路径
    if (mode === 'request') {
      await this.runSingleProbeRequest(params, windowDays, correlationId)
    }
    else {
      await this.runSingleProbePlaywright(params, windowDays, correlationId)
    }
  }

  /**
   * 运行单次探测（Playwright 模式）
   *
   * @description 运行单次探测任务（Playwright 模式）
   *
   * @param {MonitorStartParams} params - 探测启动参数
   * @param {number} windowDays - 探测窗口天数
   * @returns {Promise<void>} 无返回值
   */
  private async runSingleProbePlaywright(params: MonitorStartParams, windowDays: number, correlationId?: string): Promise<void> {
    this.ensureRunning()
    const { monitor, candidates, concurrency } = params
    // 预留配置读取（目前不使用，但保留调用方便后续扩展）
    const headless = params.options?.headless ?? true
    const timeout = params.options?.timeoutMs ?? 30000

    // 持久化：复用已存在的引擎与页面；若无或不在日历页，则导航一次到日历
    // 注意：监控探测必须使用代理，BookingEngineForBooking 会通过 BookingBrowserManager 自动选择代理
    if (!this.engine) {
      this.engine = new BookingEngineForBooking()
      this.logger.info('📡 监控探测：创建新的浏览器引擎（将自动选择代理）', { correlationId })
    }
    const engine = this.engine

    let page = engine.getPage()

    const restartFromZero = async (tag: string): Promise<boolean> => {
      try {
        try {
          const p = engine.getPage()
          if (p) {
            // 使用监控会话的专属目录快照
            try {
              const bm = engine.getBrowserManager?.()
              await bm?.saveSnapshot?.(`monitor_${tag}`)
            }
            catch {}
          }
        }
        catch {}
        try { await (engine as any)?.browser?.close?.() }
        catch {}
        // 用新的引擎自举到日历页（将自动选择新代理）
        this.engine = new BookingEngineForBooking()
        this.logger.info('📡 监控探测：重启浏览器引擎（将选择新代理）', { 阶段: tag, correlationId })
        const ctx: any = {
          user_info: {
            passport_number: monitor.passport_number,
            license_number: monitor.license_number,
            license_owner_name: monitor.full_name,
            email: monitor.email,
            phone_number: monitor.phone_number || '',
            issuing_country: monitor.issuing_country,
            issuing_authority: monitor.issuing_authority,
          },
          preferred_dates: [],
          preferred_times: [],
          options: { headless, timeout },
        }
        const opened = await this.engine.openToCalendar(ctx)
        if (!opened.success) {
          this.logger.warn('监控重启失败：无法进入日历页', { 阶段: tag })
          this.engine = null
          return false
        }
        page = this.engine.getPage()!
        this.logger.info('监控已重启并回到日历页', { 阶段: tag, correlationId })
        return true
      }
      catch (e) {
        this.logger.warn('监控重启异常', { 阶段: tag, error: (e as any)?.message || String(e) })
        return false
      }
    }
    const ensureAtCalendar = async (): Promise<boolean> => {
      try {
        const p = engine.getPage()
        if (!p)
          return false
        const url = p.url()
        if (/cmdGetCalendarAction/i.test(url))
          return true
        const html = await p.content()
        // 输入框也可辅助判断当前月
        if (/storedAppointmentCalendar(Year|Month)/.test(html) || /onClickDate\('/.test(html))
          return true
        return false
      }
      catch { return false }
    }

    if (!page || !(await ensureAtCalendar())) {
      this.ensureRunning()
      const ok = await restartFromZero('not_on_calendar')
      if (!ok)
        return
    }
    this.report({ phase: 'info', step: '已到达日历', message: '进入日历页', progress: 30 })

    // 到达日历页，扫描窗口
    const startDate = new Date(); startDate.setHours(0, 0, 0, 0)
    const endDate = new Date(startDate.getTime() + windowDays * 24 * 60 * 60 * 1000)
    const months = buildMonthsRange(startDate, endDate)
    // 调试注入：仅在开发环境生效（NODE_ENV !== 'production'）
    const nodeEnv = String(process.env.NODE_ENV || '').toLowerCase()
    const debugEnabled = nodeEnv !== 'production'
    const envMock = debugEnabled ? String(process.env.DEBUG_MONITOR_MOCK_DATES || '').trim() : ''
    const envForce = debugEnabled ? String(process.env.DEBUG_MONITOR_FORCE_FOUND || '').trim() : ''
    let debugMockDates: string[] = []
    if (debugEnabled && envMock) {
      debugMockDates = envMock.split(/[;,\s]+/).map(s => s.trim()).filter(Boolean)
      // 兼容 YYYY-MM-DD 与 DD-MM-YYYY，统一为 DD-MM-YYYY
      const toDMY = (v: string) => {
        const s = String(v || '').trim()
        const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/)
        return m ? `${m[3]}-${m[2]}-${m[1]}` : s
      }
      debugMockDates = debugMockDates.map(toDMY)
    }
    const optDebug = debugEnabled ? (params.options as any)?.debug : null
    if (debugEnabled && optDebug && Array.isArray(optDebug.mockAvailableDates))
      debugMockDates = optDebug.mockAvailableDates
    const debugForceFound = debugEnabled && ((optDebug && !!optDebug.forceFound) || envForce === '1')
    const useRandomRelease = debugEnabled && String(process.env.DEBUG_MONITOR_RANDOM_RELEASE || '').trim() === '1'
    const randDatesMinRaw = debugEnabled ? Number.parseInt(String(process.env.DEBUG_MONITOR_RAND_DATES_MIN || ''), 10) : Number.NaN
    const randDatesMaxRaw = debugEnabled ? Number.parseInt(String(process.env.DEBUG_MONITOR_RAND_DATES_MAX || ''), 10) : Number.NaN
    const randQuotaMinRaw = debugEnabled ? Number.parseInt(String(process.env.DEBUG_MONITOR_RAND_QUOTA_MIN || ''), 10) : Number.NaN
    const randQuotaMaxRaw = debugEnabled ? Number.parseInt(String(process.env.DEBUG_MONITOR_RAND_QUOTA_MAX || ''), 10) : Number.NaN
    const randDatesMin = Number.isFinite(randDatesMinRaw) ? Math.max(1, randDatesMinRaw) : 2
    const randDatesMax = Number.isFinite(randDatesMaxRaw) ? Math.max(randDatesMin, randDatesMaxRaw) : 4
    const randQuotaMin = Number.isFinite(randQuotaMinRaw) ? Math.max(1, randQuotaMinRaw) : 1
    const randQuotaMax = Number.isFinite(randQuotaMaxRaw) ? Math.max(randQuotaMin, randQuotaMaxRaw) : 3
    // 按实际规则：网站可预约日期恒在未来 70 天窗口内，因此随机放号覆盖窗口内全部月份，无需额外范围变量
    const genQuotasDMY: Record<string, number> = {}
    let current = await readCal(page)
    if (!current || current.year === undefined || current.m0 === undefined) {
      const ok = await restartFromZero('readcal_invalid')
      if (!ok)
        return
      current = await readCal(page)
      if (!current || current.year === undefined || current.m0 === undefined)
        return
    }
    if (!(current.year === months[0].y && current.m0 === months[0].m0)) {
      this.ensureRunning()
      // 跳到窗口首月（复用日历助手，包含 setTimeout 触发导航避免上下文销毁）
      await gotoCalMonth(page, months[0].y, months[0].m0)
      await page.waitForLoadState('load', { timeout: 30000 })
      await page.waitForFunction(() => document.readyState === 'complete', { timeout: 15000 })
      await page.waitForTimeout(200)
      current = await readCal(page)
    }

    // 强制刷新当前月，确保读取到最新可约状态（避免沿用上一次探测的旧 DOM）
    try {
      this.ensureRunning()
      await gotoCalMonth(page, current.year, current.m0)
      await page.waitForLoadState('load', { timeout: 30000 })
      await page.waitForFunction(() => document.readyState === 'complete', { timeout: 15000 })
      await page.waitForTimeout(200)
      current = await readCal(page)
    }
    catch {}

    const monitorOnly = !!(params.options as any)?.monitorOnly
    let found: string | null = null
    const allWindowDates: string[] = []
    // 根据真实页面定位当前在窗口中的索引，避免逻辑索引与实际月份不同步
    let idx = months.findIndex(m => m.y === current.year && m.m0 === current.m0)
    if (idx < 0) {
      // 再次对齐到窗口首月
      await gotoCalMonth(page, months[0].y, months[0].m0)
      await page.waitForLoadState('load', { timeout: 30000 })
      await page.waitForFunction(() => document.readyState === 'complete', { timeout: 15000 })
      await page.waitForTimeout(200)
      current = await readCal(page)
      idx = months.findIndex(m => m.y === current.year && m.m0 === current.m0)
      if (idx < 0)
        idx = 0
    }

    while (idx < months.length) {
      this.ensureRunning()
      // 以页面真实月份为准输出监控摘要
      try {
        const cal = await analyzeCalendarSummary(page)
        // 纠偏：若解析摘要与 hidden 值不一致，优先使用 hidden 值判断，但仍打印摘要供诊断
        this.logger.info(`📆 监控: ${cal.year}年${cal.monthIndex0 + 1}月`, { 可约: cal.availableCount, 约满: cal.bookedCount })
        // 输出统计信息（统一使用解析器结果）
        this.logger.info('📊 日历统计', {
          年: cal.year,
          月: cal.monthIndex0 + 1,
          总天数: cal.daysInMonth,
          可预约天数: Number(cal.availableCount || 0),
          休息日数: Number(cal.restCount || 0),
          约满天数: Number(cal.bookedCount || 0),
          暂不可约天数: Number(cal.tempCount || 0),
        })
        this.report({ phase: 'info', step: `扫描 ${cal.year}-${String(cal.monthIndex0 + 1).padStart(2, '0')}`, message: `可约 ${cal.availableCount} 天`, progress: 40 })
      }
      catch (e) {
        try {
          const bm = this.engine?.getBrowserManager?.()
          await bm?.saveSnapshot?.('monitor_summary_error')
        }
        catch {}
        this.logger.warn('监控摘要解析失败', { error: (e as any)?.message || String(e) })
        this.report({ phase: 'warn', step: '解析失败', message: (e as any)?.message || String(e) })
      }

      // 基于最新 current 过滤窗口内日期（排除周六日）
      const within = (d: string) => { const dt = parseDDMMYYYY(d); return dt >= startDate && dt <= endDate && ![0, 6].includes(dt.getDay()) }
      // 合并调试注入的本月日期
      let effectiveAvailableDates = Array.isArray(current.availableDates) ? current.availableDates.slice() : []
      // 随机放号生成：合并到当前月的可约集合，并记录配额（覆盖未来 70 天窗口内的所有月份）
      if (useRandomRelease) {
        try {
          const gen = generateRandomRelease({
            startDate,
            endDate,
            year: current.year,
            month0: current.m0 || 0,
            countRange: [randDatesMin, randDatesMax],
            quotaRange: [randQuotaMin, randQuotaMax],
          })
          const set = new Set<string>(effectiveAvailableDates)
          for (const d of gen.availableDates)
            set.add(d)
          effectiveAvailableDates = Array.from(set)
          for (const k of Object.keys(gen.quotas))
            genQuotasDMY[k] = gen.quotas[k]
        }
        catch {}
      }
      if (debugMockDates.length > 0) {
        try {
          const mockInMonth = debugMockDates.filter((d) => {
            const dt = parseDDMMYYYY(d)
            return dt.getFullYear() === current.year && dt.getMonth() === (current.m0 || 0) && ![0, 6].includes(dt.getDay())
          })
          // 去重合并
          const set = new Set<string>(effectiveAvailableDates)
          for (const d of mockInMonth)
            set.add(d)
          effectiveAvailableDates = Array.from(set)
        }
        catch {}
      }
      const windowDates = (effectiveAvailableDates || []).filter(within)
      if (windowDates.length > 0) {
        // 收集当前月份所有命中日期（去重）
        for (const d of windowDates) {
          if (!allWindowDates.includes(d))
            allWindowDates.push(d)
        }
      }

      // DEBUG: 强制模拟可约日期（仅用于测试并发触发）
      // TODO: 测试完成后移除此分支或关闭开关：
      //  - 配置开关：config.booking.monitor.force_fake_available_date = true
      //  - 或设置环境变量：DEBUG_MONITOR_FORCE_FOUND=1
      // try {
      //   const force = true
      //   if (!found && force) {
      //     const daysInMonth = new Date(current.year, (current.m0 || 0) + 1, 0).getDate()
      //     const pickDay = Math.min(15, daysInMonth)
      //     const fake = new Date(current.year, current.m0 || 0, pickDay)
      //     if (fake.getTime() < startDate.getTime())
      //       fake.setTime(startDate.getTime())
      //     if (fake.getTime() > endDate.getTime())
      //       fake.setTime(endDate.getTime())
      //     const dd = String(fake.getDate()).padStart(2, '0')
      //     const mm = String(fake.getMonth() + 1).padStart(2, '0')
      //     const yyyy = fake.getFullYear()
      //     found = `${dd}-${mm}-${yyyy}`
      //     this.logger.warn('⚠️ DEBUG_FORCE_FOUND: 使用伪造可用日期触发并发', { 日期: found })
      //     break
      //   }
      // }
      // catch {}
      // END DEBUG

      if (!monitorOnly && windowDates.length > 0) { found = windowDates[0]; break }
      // 若开启强制命中且无窗口日期，则从调试日期中选取窗口内的第一个
      if (!monitorOnly && !found && debugForceFound && debugMockDates.length > 0) {
        const mockWindow = debugMockDates.filter(within)
        if (mockWindow.length > 0) { found = mockWindow[0]; break }
      }

      // 翻向下一个窗口月
      if (idx + 1 >= months.length)
        break
      const target = months[idx + 1]
      this.ensureRunning()
      await gotoCalMonth(page, target.y, target.m0)
      await page.waitForLoadState('load', { timeout: 30000 })
      await page.waitForFunction(() => document.readyState === 'complete', { timeout: 15000 })
      await page.waitForTimeout(200)
      const after = await readCal(page)
      if (!(after.year === target.y && after.m0 === target.m0)) {
        // 再尝试一次兜底翻月
        this.ensureRunning()
        await gotoCalMonth(page, target.y, target.m0)
        await page.waitForLoadState('load', { timeout: 30000 })
        await page.waitForFunction(() => document.readyState === 'complete', { timeout: 15000 })
        await page.waitForTimeout(200)
        const after2 = await readCal(page)
        if (!(after2.year === target.y && after2.m0 === target.m0)) {
          try {
            const bm = this.engine?.getBrowserManager?.()
            await bm?.saveSnapshot?.('monitor_month_align_failed')
          }
          catch {}
          // 若仍未到达目标月，则以实际到达的月份在窗口中的位置继续，避免停留在同一月重复打印
          const realIdx = months.findIndex(m => m.y === after2.year && m.m0 === after2.m0)
          if (realIdx >= 0)
            idx = realIdx
          else
            idx++ // 找不到则强行推进，避免死循环
          current = after2
          continue
        }
        current = after2
        idx++
        continue
      }
      current = after
      idx++
    }

    if (monitorOnly) {
      if (allWindowDates.length === 0) {
        this.logger.info('监控：窗口内暂无可预约日期', { correlationId })
        this.report({ phase: 'info', step: '暂无可约', message: '窗口内暂无可预约日期', progress: 50 })
        return
      }

      const toYMD = (dmy: string): string => {
        try {
          const dt = parseDDMMYYYY(dmy)
          const y = dt.getFullYear()
          const m = String(dt.getMonth() + 1).padStart(2, '0')
          const d = String(dt.getDate()).padStart(2, '0')
          return `${y}-${m}-${d}`
        }
        catch { return dmy }
      }
      const datesYMD = Array.from(new Set(allWindowDates.map(toYMD))).sort()
      const ymdToDmy: Record<string, string> = {}
      for (const d of allWindowDates) {
        try { ymdToDmy[toYMD(d)] = d }
        catch {}
      }
      const quotasYMD: Record<string, number> = {}
      for (const y of datesYMD) {
        const dmy = ymdToDmy[y]; const q = (dmy && typeof genQuotasDMY[dmy] === 'number') ? genQuotasDMY[dmy] : undefined; if (typeof q === 'number')
          quotasYMD[y] = q
      }
      try {
        const rr = getReleaseRecordService()
        for (const d of datesYMD) {
          await rr.record({
            dateYMD: d,
            office_code: monitor.office_code as any,
            service_type: monitor.service_type as any,
            reason: 'monitor_only',
            range_start: (params.options as any)?.allowedDateRange?.start,
            range_end: (params.options as any)?.allowedDateRange?.end,
            monitor_id: monitor.id,
            extra: { found_dmy: allWindowDates, quotas: quotasYMD },
          })
        }
      }
      catch {}

      this.logger.info('监控：仅监控模式记录放号日期', { 数量: datesYMD.length, 日期: datesYMD, correlationId })

      this.report({
        phase: 'success',
        step: '放号记录',
        message: `记录放号 ${datesYMD.length} 天：${datesYMD.join(', ')}`,
        progress: 70,
        extra: { release: { timestamp: Math.floor(Date.now() / 1000), dates: datesYMD, allowed: (params.options as any)?.allowedDateRange || null, reason: 'monitor_only', quotas: quotasYMD } },
      })
      return
    }

    if (!found) {
      this.logger.info('监控：窗口内暂无可预约日期', { correlationId })
      this.report({ phase: 'info', step: '暂无可约', message: '窗口内暂无可预约日期', progress: 50 })
      return
    }

    // 发现可约：仅依据预约页面会话的允许日期范围判断是否下单（不读取全局配置）
    const toYMD = (dmy: string): string => {
      try {
        const dt = parseDDMMYYYY(dmy)
        const y = dt.getFullYear()
        const m = String(dt.getMonth() + 1).padStart(2, '0')
        const d = String(dt.getDate()).padStart(2, '0')
        return `${y}-${m}-${d}`
      }
      catch { return dmy }
    }
    const parseYMD = (s?: string): number | null => {
      if (!s)
        return null
      const t = String(s).trim()
      if (!t)
        return null
      if (/^\d{4}-\d{2}-\d{2}$/.test(t))
        return new Date(`${t}T00:00:00`).getTime()
      // 支持 DD-MM-YYYY
      const m = t.match(/^(\d{2})-(\d{2})-(\d{4})$/)
      if (m)
        return new Date(`${m[3]}-${m[2]}-${m[1]}T00:00:00`).getTime()
      return null
    }
    // 依据“预约页面”的允许日期范围（会话级）判定是否跳过预约并记录放号
    const allowed = (params.options as any)?.allowedDateRange
    if (allowed && (allowed.start || allowed.end)) {
      const ymd = toYMD(found)
      const dTs = new Date(`${ymd}T00:00:00`).getTime()
      const sTs = parseYMD(allowed.start)
      const eTs = parseYMD(allowed.end)
      const inAllowed = (sTs === null || dTs >= sTs) && (eTs === null || dTs <= eTs)
      if (!inAllowed) {
        try {
          // 批量记录当前窗口已发现的所有日期（去重转 YMD）
          const datesYMD = Array.from(new Set((allWindowDates.length > 0 ? allWindowDates : [found]).map(toYMD))).sort()
          const rr = getReleaseRecordService()
          for (const d of datesYMD) {
            await rr.record({
              dateYMD: d,
              office_code: monitor.office_code as any,
              service_type: monitor.service_type as any,
              reason: 'out_of_allowed_date_range',
              range_start: allowed.start,
              range_end: allowed.end,
              monitor_id: monitor.id,
              extra: { found_dmy: allWindowDates.length > 0 ? allWindowDates : [found] },
            })
          }
          this.logger.info('已记录放号但不预约（不在允许日期范围）', { 日期: datesYMD, 范围: allowed, correlationId })
        }
        catch {}
        this.report({
          phase: 'info',
          step: '放号记录',
          message: `不在允许日期范围，已记录多日`,
          progress: 60,
          extra: { release: { timestamp: Math.floor(Date.now() / 1000), dates: Array.from(new Set((allWindowDates.length > 0 ? allWindowDates : [found]).map(toYMD))).sort(), allowed, reason: 'out_of_allowed_date_range' } },
        })
        // 不关闭引擎，继续监控
        return
      }
    }

    // 发现可约且满足范围：释放监控占用，切换为目标并发并发起预约
    this.logger.info('🎯 监控发现可预约日期，触发并发预约', { 日期: found, 目标并发: concurrency, 数量: candidates.length, correlationId })
    this.report({ phase: 'success', step: '发现可约', message: `日期 ${found}，准备触发并发预约`, progress: 80 })
    await (engine as any)?.browser?.close?.()
    this.engine = null

    try { limiter.setLimit(concurrency) }
    catch {}

    const svc = getUnifiedBookingService()
    const appSvc = getAppointmentService()
    // 简易并发执行（与 IPC 逻辑一致）
    let running = 0
    let idxLaunch = 0
    const results: Array<{ id: number, success: boolean, error?: string }> = []
    await new Promise<void>((resolveAll) => {
      const launchNext = () => {
        if (!this.running) { resolveAll(); return }
        while (this.running && running < concurrency && idxLaunch < candidates.length) {
          const item = candidates[idxLaunch++]
          running++
          ;(async () => {
            try {
              // 使用options中指定的模式，默认为playwright（因为监控本身使用Playwright引擎探测）
              const bookingMode = params.options?.mode || 'playwright'
              // 将强制范围作为会话的 allowedDateRange 传入，以确保引擎内部也不会越界选择
              const r = await svc.bookWith(bookingMode, item, {
                headless: params.options?.headless ?? true,
                timeoutMs: params.options?.timeoutMs ?? 30000,
                // 来自预约页面设置的会话范围；策略层会对强制模式自动忽略
                allowedDateRange: (params.options as any)?.allowedDateRange,
              })
              // 成功/失败持久化（与 IPC 逻辑一致）
              try {
                if (r.success) {
                  await appSvc.updateAppointment(item.id, {
                    appointment_status: 'booked',
                    reference_number: r.referenceNumber,
                    appointment_date: r.selectedDate,
                    appointment_time: normalizeAppointmentTime(r.selectedTimeslot),
                  } as any)
                }
                else {
                  await appSvc.updateAppointment(item.id, {
                    appointment_status: 'failed',
                    error_message: r.error || 'booking failed',
                  } as any)
                }
              }
              catch (persistErr: any) {
                this.logger.warn('监控触发的预约结果写库失败', { id: item.id, error: persistErr?.message || String(persistErr) })
              }
              results.push({ id: item.id, success: r.success, error: r.error })
            }
            catch (e: any) {
              results.push({ id: item.id, success: false, error: e?.message || String(e) })
            }
            finally {
              running--
              if (!this.running) {
                resolveAll()
              }
              else if (idxLaunch >= candidates.length && running <= 0) {
                resolveAll()
              }
              else {
                launchNext()
              }
            }
          })()
        }
      }
      launchNext()
    })

    const ok = results.filter(r => r.success).length
    this.logger.info('监控触发的并发预约完成', { total: results.length, success: ok, failed: results.length - ok, correlationId })
  }

  /**
   * 运行单次探测（Request 模式）
   *
   * @description 运行单次探测任务（Request 模式）
   *
   * @param {MonitorStartParams} params - 探测启动参数
   * @param {number} windowDays - 探测窗口天数
   * @returns {Promise<void>} 无返回值
   */
  private async runSingleProbeRequest(params: MonitorStartParams, windowDays: number, correlationId?: string): Promise<void> {
    this.ensureRunning()
    const { monitor, candidates } = params
    // concurrency 参数保留用于未来扩展，当前通过任务执行器控制并发

    // 创建或复用 Request 模式的 session 和 flows
    if (!this.session || !this.flows) {
      try {
        // 获取代理（监控必须使用代理）
        const proxy = selectProxyForRequestSession()
        if (!proxy) {
          this.logger.error('监控探测失败：无可用代理（监控必须使用代理）')
          this.report({ phase: 'error', step: '代理选择失败', message: '无可用代理，监控必须使用代理' })
          // 等待一段时间后重试，避免快速失败循环
          await this.delayCancellable(10000)
          return
        }

        this.logger.info('监控使用代理', {
          server: proxy.server,
          provider: proxy.providerLabel || proxy.providerId,
          poolSize: proxy.poolSize,
          correlationId,
        })

        const proxyForSession = {
          server: proxy.server,
          username: proxy.username,
          password: proxy.password,
        }

        // Request 模式监控选择原生或undici版本
        const envForceOriginal = String((process as any)?.env?.FORCE_ORIGINAL_HTTP || '').trim() === '1'
          || String((process as any)?.env?.NETWORK_USE_UNDICI || '').trim() === '0'
        const useUndici = !envForceOriginal && (getConfig().network?.use_undici !== false)
        this.session = useUndici
          ? new HttpSessionV2(undefined, proxyForSession)
          : new HttpSession(undefined, proxyForSession)

        // 监控探测使用“随机身份”避免同账号触发 512-E-0044
        const probeId = Math.random().toString(36).slice(2, 8)
        const randDigits = (n: number) => Array.from({ length: n }, () => Math.floor(Math.random() * 10)).join('')
        const randomPassport = `P${randDigits(8)}`
        const randomLicense = `L${randDigits(8)}`
        const randomName = `MonitorProbe-${probeId}`
        this.monitorIdentity = { passport: randomPassport, license: randomLicense, name: randomName, probeId }
        const appointmentMetadata = {
          appointmentId: monitor.id,
          passportNumber: this.monitorIdentity.passport,
          licenseNumber: this.monitorIdentity.license,
          fullName: this.monitorIdentity.name,
          sequenceNumber: monitor.sequence_number,
          isMonitor: true,
        }
        this.flows = new TdabsFlows(this.session, appointmentMetadata)
        this.logger.info('📡 监控探测（Request模式）：创建新的会话（HttpSessionV2，已选择代理）', { correlationId })
      }
      catch (e) {
        this.logger.warn('监控探测失败：创建Request会话失败', { error: (e as any)?.message || String(e) })
        this.report({ phase: 'error', step: '会话创建失败', message: (e as any)?.message || String(e) })
        return
      }
    }

    const flows = this.flows
    const session = this.session
    const parser = getPageParser()
    const cfg = getConfig()
    const baseUrl = cfg.network.base_url
    const url = (cmd: string) => cmd.startsWith('?') ? `${baseUrl}${cmd}` : `${baseUrl}?${cmd}`
    const baseForm = (extra?: Record<string, string>) => ({
      lang: 'tchinese',
      timestamp: Date.now().toString(),
      ...(extra || {}),
    })

    // 初始化到日历页（如果需要）
    const initializeToCalendar = async (): Promise<{ html: string, formFields: Record<string, string> } | null> => {
      try {
        this.ensureRunning()
        // 访问首页
        await flows.navigateHome()
        this.report({ phase: 'info', step: '访问首页', message: 'Request模式访问首页', progress: 15 })

        // 同意条款
        const personalInfo = await flows.agreeTermsAndEnterPersonalInfo()
        this.report({ phase: 'info', step: '同意条款', message: 'Request模式同意条款', progress: 20 })

        // 提交个人信息和身份验证
        const verifyResult = await flows.submitPersonalInfoAndVerifyIdentity(
          personalInfo.html,
          {
            passport: this.monitorIdentity?.passport || `P${Math.floor(Math.random() * 1e8).toString().padStart(8, '0')}`,
            license: this.monitorIdentity?.license || `L${Math.floor(Math.random() * 1e8).toString().padStart(8, '0')}`,
          },
          {
            name: this.monitorIdentity?.name || `MonitorProbe-${Math.random().toString(36).slice(2, 8)}`,
            email: `monitor+${this.monitorIdentity?.probeId || Math.random().toString(36).slice(2, 8)}@example.com`,
            phone: '00000000',
            issuingCountry: monitor.issuing_country,
            issuingAuthority: monitor.issuing_authority,
          },
        )
        this.report({ phase: 'info', step: '身份验证', message: 'Request模式身份验证完成', progress: 25 })

        // 检查是否需要选择事务处
        let currentHtml = verifyResult.html
        let currentUrl = verifyResult.url

        // 处理事务处选择页
        if (currentHtml.includes('storedAppointmentOfficeCode') || currentHtml.includes('請選擇事務處') || currentHtml.includes('请选择事务处')) {
          const officeFields = parser.extractFormFields(currentHtml, 'frmAppointment')
          const officeCode = monitor.office_code || 'HKLO'
          const officeForm = {
            ...(officeFields.success && officeFields.data ? officeFields.data : {}),
            ...baseForm({ storedAppointmentOperation: 'A' }),
            storedAppointmentOfficeCode: officeCode,
          }
          const officeRes = await session.postForm(
            url('cmd=cmdGetCalendarAction'),
            officeForm,
            currentUrl,
          )
          currentHtml = officeRes.body
          currentUrl = officeRes.url
        }

        // 获取日历（复用预约逻辑）
        const hasCalendarHeader = currentHtml.includes('calendar-table-header-cell')
        const hasDateLinks = currentHtml.includes('onClickDate')
        const hasCalendarTable = currentHtml.includes('calendar-table') || currentHtml.includes('calendar-inner-table')
        const hasForm = /form[^>]+name=["']frmAppointment["']/i.test(currentHtml)
        const isAlreadyCalendar = currentUrl.includes('cmdGetCalendarAction')
          && hasForm && (hasCalendarHeader || (hasDateLinks && hasCalendarTable))

        let resCalendar: { body: string, url: string }
        if (!isAlreadyCalendar) {
          // 如果不在日历页，提交表单获取日历
          const form1Fields = parser.extractFormFields(currentHtml, 'frmAppointment')
          const form1 = {
            ...(form1Fields.success && form1Fields.data ? form1Fields.data : {}),
            ...baseForm({ storedAppointmentOperation: 'A' }),
          }
          const calendarRes = await session.postForm(
            url('cmd=cmdGetCalendarAction'),
            form1,
            currentUrl,
          )
          resCalendar = { body: calendarRes.body, url: calendarRes.url }
        }
        else {
          // 已经在日历页，直接使用当前HTML
          resCalendar = { body: currentHtml, url: currentUrl }
        }

        // 错误页检测（如 512-E-0044 重覆登入/系统中断）
        try {
          const err = parser.detectError(resCalendar.body)
          if (err?.success) {
            const code = String((err.data as any)?.code || '').toUpperCase()
            const msg = String((err.data as any)?.message || '')
            const isDupLogin = code === 'DUPLICATE_LOGIN' || /512-E-0044|重[複覆]登入|系统而中断|系統而中斷/i.test(`${code} ${msg}`)
            if (isDupLogin) {
              try { (session as any)?.banCurrentProxy?.('512-E-0044') }
              catch {}
              this.logger.warn('监控探测被站点阻挡（512-E-0044）', { correlationId })
              this.report({ phase: 'error', step: '站点阻挡', message: '被系统暂时阻挡（512-E-0044），将切换出口稍后重试' })
              // 指数回退（不占用并发槽位）
              this.initRetryCount = Math.min(this.initRetryCount + 1, 5)
              const backoff = this.computeInitBackoffMs('512-E-0044', this.initRetryCount)
              try { await this.delayCancellable(backoff) }
              catch {}
              return null
            }
          }
        }
        catch {}

        // 验证是否在日历页（使用 parser 解析）
        const datesResult = parser.parseAvailableDates(resCalendar.body)
        if (!datesResult.success) {
          this.logger.warn('监控探测失败：无法进入日历页（Request模式）')
          this.report({ phase: 'error', step: '日历页获取失败', message: '无法进入日历页' })
          // 解析失败也做短回退
          this.initRetryCount = Math.min(this.initRetryCount + 1, 5)
          const backoff = this.computeInitBackoffMs('parse_failed', this.initRetryCount)
          try { await this.delayCancellable(backoff) }
          catch {}
          return null
        }

        // 提取表单字段供后续切换月份使用
        const formFields = parser.extractFormFields(resCalendar.body, 'frmAppointment')
        const currentFormFields = formFields.success && formFields.data ? formFields.data : {}

        this.initRetryCount = 0
        this.logger.info('监控已进入日历页（Request模式）', { correlationId })
        this.report({ phase: 'info', step: '已到达日历', message: 'Request模式进入日历页', progress: 30 })
        return { html: resCalendar.body, formFields: currentFormFields }
      }
      catch (e) {
        this.logger.warn('监控初始化失败（Request模式）', { error: (e as any)?.message || String(e) })
        this.report({ phase: 'error', step: '初始化失败', message: (e as any)?.message || String(e) })
        // 重置会话和状态，下次重试
        this.flows = null
        this.session = null
        this.currentCalendarState = null
        // 根据错误类型回退
        try {
          const msg = String((e as any)?.message || '')
          this.initRetryCount = Math.min(this.initRetryCount + 1, 5)
          const backoff = this.computeInitBackoffMs(msg, this.initRetryCount)
          await this.delayCancellable(backoff)
        }
        catch {}
        return null
      }
    }

    // Request 模式监控：复用会话状态，在当前月份基础上继续扫描
    // 只有在没有保存状态或遇到异常时才重新初始化
    let currentCalendarHtml: string | null = null
    let currentFormFields: Record<string, string> = {}
    let needReinit = false

    // 检查是否有保存的日历状态
    if (this.currentCalendarState) {
      // 复用保存的状态
      currentCalendarHtml = this.currentCalendarState.html
      currentFormFields = this.currentCalendarState.formFields
      this.logger.debug('复用上次日历状态', {
        year: this.currentCalendarState.year,
        month: this.currentCalendarState.month0 + 1,
      })
    }
    else {
      // 没有保存状态，需要初始化
      needReinit = true
    }

    // 如果需要初始化，执行初始化流程
    if (needReinit) {
      const initResult = await initializeToCalendar()
      if (!initResult) {
        // 初始化失败，返回让外层循环等待 interval 后重试
        this.currentCalendarState = null // 清除无效状态
        return
      }
      currentCalendarHtml = initResult.html
      currentFormFields = initResult.formFields
    }

    // 到达日历页，扫描窗口
    // 再次错误页检测（保存状态后首次解析前）
    try {
      const parser = getPageParser()
      const err = parser.detectError(currentCalendarHtml || '')
      if (err?.success) {
        const code = String((err.data as any)?.code || '').toUpperCase()
        const msg = String((err.data as any)?.message || '')
        const isDupLogin = code === 'DUPLICATE_LOGIN' || /512-E-0044|重[複覆]登入|系统而中断|系統而中斷/i.test(`${code} ${msg}`)
        if (isDupLogin) {
          try { (this.session as any)?.banCurrentProxy?.('512-E-0044') }
          catch {}
          this.logger.warn('监控探测被站点阻挡（保存后校验，512-E-0044）')
          this.report({ phase: 'error', step: '站点阻挡', message: '被系统暂时阻挡（512-E-0044），将切换出口稍后重试' })
          // 清理状态，下次循环重新初始化
          this.currentCalendarState = null
          return
        }
      }
    }
    catch {}

    const startDate = new Date(); startDate.setHours(0, 0, 0, 0)
    const endDate = new Date(startDate.getTime() + windowDays * 24 * 60 * 60 * 1000)
    const months = buildMonthsRange(startDate, endDate)

    // 调试注入（与 Playwright 模式一致）
    const nodeEnv = String(process.env.NODE_ENV || '').toLowerCase()
    const debugEnabled = nodeEnv !== 'production'
    const envMock = debugEnabled ? String(process.env.DEBUG_MONITOR_MOCK_DATES || '').trim() : ''
    const envForce = debugEnabled ? String(process.env.DEBUG_MONITOR_FORCE_FOUND || '').trim() : ''
    let debugMockDates: string[] = []
    if (debugEnabled && envMock) {
      debugMockDates = envMock.split(/[;,\s]+/).map(s => s.trim()).filter(Boolean)
      const toDMY = (v: string) => {
        const s = String(v || '').trim()
        const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/)
        return m ? `${m[3]}-${m[2]}-${m[1]}` : s
      }
      debugMockDates = debugMockDates.map(toDMY)
    }
    const optDebug = debugEnabled ? (params.options as any)?.debug : null
    if (debugEnabled && optDebug && Array.isArray(optDebug.mockAvailableDates))
      debugMockDates = optDebug.mockAvailableDates
    const debugForceFound = debugEnabled && ((optDebug && !!optDebug.forceFound) || envForce === '1')
    const useRandomRelease = debugEnabled && String(process.env.DEBUG_MONITOR_RANDOM_RELEASE || '').trim() === '1'
    const randDatesMinRaw = debugEnabled ? Number.parseInt(String(process.env.DEBUG_MONITOR_RAND_DATES_MIN || ''), 10) : Number.NaN
    const randDatesMaxRaw = debugEnabled ? Number.parseInt(String(process.env.DEBUG_MONITOR_RAND_DATES_MAX || ''), 10) : Number.NaN
    const randQuotaMinRaw = debugEnabled ? Number.parseInt(String(process.env.DEBUG_MONITOR_RAND_QUOTA_MIN || ''), 10) : Number.NaN
    const randQuotaMaxRaw = debugEnabled ? Number.parseInt(String(process.env.DEBUG_MONITOR_RAND_QUOTA_MAX || ''), 10) : Number.NaN
    const randDatesMin = Number.isFinite(randDatesMinRaw) ? Math.max(1, randDatesMinRaw) : 2
    const randDatesMax = Number.isFinite(randDatesMaxRaw) ? Math.max(randDatesMin, randDatesMaxRaw) : 4
    const randQuotaMin = Number.isFinite(randQuotaMinRaw) ? Math.max(1, randQuotaMinRaw) : 1
    const randQuotaMax = Number.isFinite(randQuotaMaxRaw) ? Math.max(randQuotaMin, randQuotaMaxRaw) : 3
    const genQuotasDMY: Record<string, number> = {}

    // 解析当前月份（使用更完善的检查逻辑，类似 TdabsFlows.analyzeCalendarSummary）
    const parseCalendarMonth = (html: string): { year: number, monthIndex0: number } | null => {
      try {
        const $ = cheerio.load(html)
        // 检查是否是真正的日历页面
        const hasCalendarHeader = $('.calendar-table-header-cell').length > 0
        const hasCalendarYearField = $('input[name="storedAppointmentCalendarYear"]').length > 0
        const hasCalendarMonthField = $('input[name="storedAppointmentCalendarMonth"]').length > 0
        const hasDateLinks = $('a[onclick*="onClickDate"]').length > 0
        const hasCalendarTable = $('.calendar-table, .calendar-inner-table').length > 0

        if (!hasCalendarHeader && !(hasCalendarYearField && hasCalendarMonthField) && !hasDateLinks && !hasCalendarTable) {
          return null
        }

        // 检测年月
        const yField = $('input[name="storedAppointmentCalendarYear"]').val() as string | undefined
        const mField = $('input[name="storedAppointmentCalendarMonth"]').val() as string | undefined
        if (yField && mField) {
          const y = Number.parseInt(String(yField), 10)
          const m = Number.parseInt(String(mField), 10)
          if (!Number.isNaN(y) && !Number.isNaN(m))
            return { year: y, monthIndex0: m }
        }

        // 从日历表头解析
        const headerEl = $('.calendar-table-header-cell').first()
        const headerText = headerEl.text() || ''
        const mh = headerText.match(/(\d{4})年\s*(\d{1,2})月/)
        if (mh) {
          const y = Number.parseInt(mh[1], 10)
          const m = Number.parseInt(mh[2], 10) - 1
          if (!Number.isNaN(y) && !Number.isNaN(m))
            return { year: y, monthIndex0: m }
        }

        return null
      }
      catch {
        return null
      }
    }

    const currentSummary = parseCalendarMonth(currentCalendarHtml || '')
    if (!currentSummary) {
      this.logger.warn('监控探测失败：无法解析日历页')
      return
    }

    let currentYear = currentSummary.year
    let currentMonth0 = currentSummary.monthIndex0

    // 对齐到窗口首月
    if (!(currentYear === months[0].y && currentMonth0 === months[0].m0)) {
      this.ensureRunning()
      try {
        // 使用保存的表单字段切换月份
        const monthForm = {
          ...currentFormFields,
          ...baseForm({ storedAppointmentOperation: 'A' }),
          storedAppointmentCalendarYear: months[0].y.toString(),
          storedAppointmentCalendarMonth: months[0].m0.toString(),
        }
        const monthRes = await session.postForm(
          url('cmd=cmdGetCalendarAction'),
          monthForm,
          url('cmd=cmdGetCalendarAction'),
        )
        currentCalendarHtml = monthRes.body
        // 更新表单字段
        const formFields = parser.extractFormFields(monthRes.body, 'frmAppointment')
        if (formFields.success && formFields.data) {
          currentFormFields = { ...currentFormFields, ...formFields.data }
        }
        else {
          // 表单字段提取失败，可能不在日历页，重新初始化
          this.logger.warn('对齐到窗口首月时表单字段提取失败，重新初始化到日历页')
          const reinitResult = await initializeToCalendar()
          if (!reinitResult) {
            this.logger.warn('监控探测失败：重新初始化失败')
            return
          }
          currentCalendarHtml = reinitResult.html
          currentFormFields = reinitResult.formFields
        }

        const newSummary = parseCalendarMonth(currentCalendarHtml || '')
        if (newSummary) {
          currentYear = newSummary.year
          currentMonth0 = newSummary.monthIndex0
        }
        else {
          // 无法解析日历页，重新初始化
          this.logger.warn('对齐到窗口首月后无法解析日历页，重新初始化')
          const reinitResult = await initializeToCalendar()
          if (!reinitResult) {
            this.logger.warn('监控探测失败：重新初始化失败')
            return
          }
          currentCalendarHtml = reinitResult.html
          currentFormFields = reinitResult.formFields
          const reinitSummary = parseCalendarMonth(currentCalendarHtml || '')
          if (reinitSummary) {
            currentYear = reinitSummary.year
            currentMonth0 = reinitSummary.monthIndex0
          }
          else {
            this.logger.warn('监控探测失败：重新初始化后仍无法解析日历页')
            return
          }
        }
      }
      catch (err) {
        // 切换月份失败，重新初始化
        this.logger.warn('对齐到窗口首月时发生错误，重新初始化', { error: err })
        const reinitResult = await initializeToCalendar()
        if (!reinitResult) {
          this.logger.warn('监控探测失败：重新初始化失败')
          return
        }
        currentCalendarHtml = reinitResult.html
        currentFormFields = reinitResult.formFields
        const reinitSummary = parseCalendarMonth(currentCalendarHtml || '')
        if (reinitSummary) {
          currentYear = reinitSummary.year
          currentMonth0 = reinitSummary.monthIndex0
        }
        else {
          this.logger.warn('监控探测失败：重新初始化后仍无法解析日历页')
          return
        }
      }
    }

    const monitorOnly = !!(params.options as any)?.monitorOnly
    let found: string | null = null
    const allWindowDates: string[] = []

    // 确定起始索引：始终从“对齐后的当前月份（窗口首月）”开始扫描
    // 复用保存状态仅用于避免重新初始化，不用于决定扫描起点
    let idx = months.findIndex(m => m.y === currentYear && m.m0 === currentMonth0)
    if (idx < 0)
      idx = 0
    this.logger.debug('从窗口首月开始扫描', { year: currentYear, month: currentMonth0 + 1, index: idx })

    // 扫描所有月份
    while (idx < months.length) {
      this.ensureRunning()
      const targetMonth = months[idx]

      // 如果不是当前月份，切换到目标月份
      if (!(currentYear === targetMonth.y && currentMonth0 === targetMonth.m0)) {
        try {
          // 使用保存的表单字段切换月份
          const monthForm = {
            ...currentFormFields,
            ...baseForm({ storedAppointmentOperation: 'A' }),
            storedAppointmentCalendarYear: targetMonth.y.toString(),
            storedAppointmentCalendarMonth: targetMonth.m0.toString(),
          }
          const monthRes = await session.postForm(
            url('cmd=cmdGetCalendarAction'),
            monthForm,
            url('cmd=cmdGetCalendarAction'),
          )
          currentCalendarHtml = monthRes.body
          // 更新表单字段
          const formFields = parser.extractFormFields(monthRes.body, 'frmAppointment')
          if (formFields.success && formFields.data) {
            currentFormFields = { ...currentFormFields, ...formFields.data }
          }
          else {
            // 表单字段提取失败，可能不在日历页，重新初始化
            this.logger.warn('切换月份后表单字段提取失败，重新初始化到日历页')
            const reinitResult = await initializeToCalendar()
            if (!reinitResult) {
              this.logger.warn('监控探测失败：重新初始化失败')
              return
            }
            currentCalendarHtml = reinitResult.html
            currentFormFields = reinitResult.formFields
          }

          const newSummary = parseCalendarMonth(currentCalendarHtml || '')
          if (newSummary) {
            currentYear = newSummary.year
            currentMonth0 = newSummary.monthIndex0
          }
          else {
            // 无法解析日历页，重新初始化
            this.logger.warn('切换月份后无法解析日历页，重新初始化')
            const reinitResult = await initializeToCalendar()
            if (!reinitResult) {
              this.logger.warn('监控探测失败：重新初始化失败')
              return
            }
            currentCalendarHtml = reinitResult.html
            currentFormFields = reinitResult.formFields
            const reinitSummary = parseCalendarMonth(currentCalendarHtml || '')
            if (reinitSummary) {
              currentYear = reinitSummary.year
              currentMonth0 = reinitSummary.monthIndex0
            }
            else {
              this.logger.warn('监控探测失败：重新初始化后仍无法解析日历页')
              return
            }
          }
        }
        catch (err) {
          // 切换月份失败，重新初始化
          this.logger.warn('切换月份时发生错误，重新初始化', { error: err })
          const reinitResult = await initializeToCalendar()
          if (!reinitResult) {
            this.logger.warn('监控探测失败：重新初始化失败')
            return
          }
          currentCalendarHtml = reinitResult.html
          currentFormFields = reinitResult.formFields
          const reinitSummary = parseCalendarMonth(currentCalendarHtml || '')
          if (reinitSummary) {
            currentYear = reinitSummary.year
            currentMonth0 = reinitSummary.monthIndex0
          }
          else {
            this.logger.warn('监控探测失败：重新初始化后仍无法解析日历页')
            return
          }
        }
      }

      // 解析当前月份（使用 parseCalendarMonth 获取年月，使用 parser.parseAvailableDates 解析日期）
      const calSummary = parseCalendarMonth(currentCalendarHtml || '')
      if (!calSummary) {
        // 无法解析日历页，重新初始化并跳过这个月份
        this.logger.warn('无法解析日历页，重新初始化')
        const reinitResult = await initializeToCalendar()
        if (!reinitResult) {
          this.logger.warn('监控探测失败：重新初始化失败')
          return
        }
        currentCalendarHtml = reinitResult.html
        currentFormFields = reinitResult.formFields
        const reinitSummary = parseCalendarMonth(currentCalendarHtml || '')
        if (reinitSummary) {
          currentYear = reinitSummary.year
          currentMonth0 = reinitSummary.monthIndex0
          // 重新计算索引，跳过当前月份
          idx = months.findIndex(m => m.y === currentYear && m.m0 === currentMonth0)
          if (idx < 0)
            idx++
          continue
        }
        else {
          this.logger.warn('监控探测失败：重新初始化后仍无法解析日历页')
          return
        }
      }

      // 解析统计（优先使用 PageParser；退化到仅可约）
      const summaryRes = parser.parseCalendarSummary(currentCalendarHtml || '')
      let availableCount = 0
      if (summaryRes.success && summaryRes.data) {
        const summary = summaryRes.data
        availableCount = summary.availableCount
        this.logger.info(`📆 监控: ${calSummary.year}年${calSummary.monthIndex0 + 1}月`, { 可约: summary.availableCount, 约满: summary.bookedCount })
        this.logger.info('📊 日历统计', { 年: summary.year, 月: summary.monthIndex0 + 1, 总天数: summary.daysInMonth, 可预约天数: summary.availableCount, 休息日数: summary.restCount, 约满天数: summary.bookedCount, 暂不可约天数: summary.tempCount })
      }
      else {
        const datesResult = parser.parseAvailableDates(currentCalendarHtml || '')
        availableCount = datesResult.success && datesResult.data ? datesResult.data.length : 0
        this.logger.info(`📆 监控: ${calSummary.year}年${calSummary.monthIndex0 + 1}月`, { 可约: availableCount })
        // 解析失败时不输出“📊 日历统计”，避免误导
      }
      this.report({ phase: 'info', step: `扫描 ${calSummary.year}-${String(calSummary.monthIndex0 + 1).padStart(2, '0')}`, message: `可约 ${availableCount} 天`, progress: 40 })
      const within = (d: string) => { const dt = parseDDMMYYYY(d); return dt >= startDate && dt <= endDate && ![0, 6].includes(dt.getDay()) }
      let effectiveAvailableDates: string[] = []
      if (summaryRes.success && summaryRes.data) {
        // 保持与 parseAvailableDates 一致的 DD-MM-YYYY 形式需要重新从 HTML 解析
        const datesResult = parser.parseAvailableDates(currentCalendarHtml || '')
        effectiveAvailableDates = datesResult.success && datesResult.data ? datesResult.data.slice() : []
      }
      else {
        const datesResult = parser.parseAvailableDates(currentCalendarHtml || '')
        effectiveAvailableDates = datesResult.success && datesResult.data ? datesResult.data.slice() : []
      }

      // 随机放号生成
      if (useRandomRelease) {
        try {
          const gen = generateRandomRelease({
            startDate,
            endDate,
            year: currentYear,
            month0: currentMonth0,
            countRange: [randDatesMin, randDatesMax],
            quotaRange: [randQuotaMin, randQuotaMax],
          })
          const set = new Set<string>(effectiveAvailableDates)
          for (const d of gen.availableDates)
            set.add(d)
          effectiveAvailableDates = Array.from(set)
          for (const k of Object.keys(gen.quotas))
            genQuotasDMY[k] = gen.quotas[k]
        }
        catch {}
      }

      // 调试注入日期
      if (debugMockDates.length > 0) {
        try {
          const mockInMonth = debugMockDates.filter((d) => {
            const dt = parseDDMMYYYY(d)
            return dt.getFullYear() === currentYear && dt.getMonth() === currentMonth0 && ![0, 6].includes(dt.getDay())
          })
          const set = new Set<string>(effectiveAvailableDates)
          for (const d of mockInMonth)
            set.add(d)
          effectiveAvailableDates = Array.from(set)
        }
        catch {}
      }

      const windowDates = effectiveAvailableDates.filter(within)
      if (windowDates.length > 0) {
        for (const d of windowDates) {
          if (!allWindowDates.includes(d))
            allWindowDates.push(d)
        }
      }

      // 监控模式：收集所有可预约日期，不中断扫描
      // 只有在非监控模式下才可能提前退出（通过found）
      if (!monitorOnly && windowDates.length > 0 && !found) {
        found = windowDates[0]
        // 注意：不再break，继续扫描所有月份以确保完整性
      }
      if (!monitorOnly && !found && debugForceFound && debugMockDates.length > 0) {
        const mockWindow = debugMockDates.filter(within)
        if (mockWindow.length > 0)
          found = mockWindow[0]
        // 注意：不再break，继续扫描所有月份
      }

      // 翻向下一个窗口月
      if (idx + 1 >= months.length)
        break
      const target = months[idx + 1]
      this.ensureRunning()
      try {
        const monthForm = {
          ...currentFormFields,
          ...baseForm({ storedAppointmentOperation: 'A' }),
          storedAppointmentCalendarYear: target.y.toString(),
          storedAppointmentCalendarMonth: target.m0.toString(),
        }
        const monthRes = await session.postForm(
          url('cmd=cmdGetCalendarAction'),
          monthForm,
          url('cmd=cmdGetCalendarAction'),
        )
        currentCalendarHtml = monthRes.body
        const formFields = parser.extractFormFields(monthRes.body, 'frmAppointment')
        if (formFields.success && formFields.data) {
          currentFormFields = { ...currentFormFields, ...formFields.data }
        }
        const newSummary = parseCalendarMonth(currentCalendarHtml || '')
        if (newSummary) {
          currentYear = newSummary.year
          currentMonth0 = newSummary.monthIndex0
        }
      }
      catch (err) {
        this.logger.warn('切换月份失败', { error: err })
      }
      idx++
    }

    // 扫描完成，保存当前日历状态供下次复用
    // 确保当前状态有效（有HTML和表单字段，且年月信息正确）
    if (currentCalendarHtml && currentFormFields) {
      const finalSummary = parseCalendarMonth(currentCalendarHtml)
      if (finalSummary) {
        this.currentCalendarState = {
          html: currentCalendarHtml,
          formFields: currentFormFields,
          year: finalSummary.year,
          month0: finalSummary.monthIndex0,
        }
        this.logger.debug('保存日历状态供下次复用', {
          year: finalSummary.year,
          month: finalSummary.monthIndex0 + 1,
        })
      }
      else {
        // 无法解析当前月份，清除状态
        this.currentCalendarState = null
      }
    }
    else {
      // 如果无法保存状态，清除旧状态
      this.currentCalendarState = null
    }

    // 处理监控结果（与 Playwright 模式一致）
    if (monitorOnly) {
      if (allWindowDates.length === 0) {
        this.logger.info('监控：窗口内暂无可预约日期')
        this.report({ phase: 'info', step: '暂无可约', message: '窗口内暂无可预约日期', progress: 50 })
        return
      }

      const toYMD = (dmy: string): string => {
        try {
          const dt = parseDDMMYYYY(dmy)
          const y = dt.getFullYear()
          const m = String(dt.getMonth() + 1).padStart(2, '0')
          const d = String(dt.getDate()).padStart(2, '0')
          return `${y}-${m}-${d}`
        }
        catch { return dmy }
      }
      const datesYMD = Array.from(new Set(allWindowDates.map(toYMD))).sort()
      const ymdToDmy: Record<string, string> = {}
      for (const d of allWindowDates) {
        try { ymdToDmy[toYMD(d)] = d }
        catch {}
      }
      const quotasYMD: Record<string, number> = {}
      for (const y of datesYMD) {
        const dmy = ymdToDmy[y]; const q = (dmy && typeof genQuotasDMY[dmy] === 'number') ? genQuotasDMY[dmy] : undefined; if (typeof q === 'number')
          quotasYMD[y] = q
      }
      try {
        const rr = getReleaseRecordService()
        for (const d of datesYMD) {
          await rr.record({
            dateYMD: d,
            office_code: monitor.office_code as any,
            service_type: monitor.service_type as any,
            reason: 'monitor_only',
            range_start: (params.options as any)?.allowedDateRange?.start,
            range_end: (params.options as any)?.allowedDateRange?.end,
            monitor_id: monitor.id,
            extra: { found_dmy: allWindowDates, quotas: quotasYMD },
          })
        }
      }
      catch {}
      this.logger.info('监控：仅监控模式记录放号日期', { 数量: datesYMD.length, 日期: datesYMD, correlationId })

      this.report({
        phase: 'success',
        step: '放号记录',
        message: `记录放号 ${datesYMD.length} 天：${datesYMD.join(', ')}`,
        progress: 70,
        extra: { release: { timestamp: Math.floor(Date.now() / 1000), dates: datesYMD, allowed: (params.options as any)?.allowedDateRange || null, reason: 'monitor_only', quotas: quotasYMD } },
      })
      return
    }

    // 处理发现的可预约日期：分发预约任务而非直接预约
    const toYMD = (dmy: string): string => {
      try {
        const dt = parseDDMMYYYY(dmy)
        const y = dt.getFullYear()
        const m = String(dt.getMonth() + 1).padStart(2, '0')
        const d = String(dt.getDate()).padStart(2, '0')
        return `${y}-${m}-${d}`
      }
      catch { return dmy }
    }
    const parseYMD = (s?: string): number | null => {
      if (!s)
        return null
      const t = String(s).trim()
      if (!t)
        return null
      if (/^\d{4}-\d{2}-\d{2}$/.test(t))
        return new Date(`${t}T00:00:00`).getTime()
      const m = t.match(/^(\d{2})-(\d{2})-(\d{4})$/)
      if (m)
        return new Date(`${m[3]}-${m[2]}-${m[1]}T00:00:00`).getTime()
      return null
    }

    // 转换所有发现的日期为 YYYY-MM-DD 格式
    const datesYMD = Array.from(new Set(allWindowDates.map(toYMD))).sort()

    if (datesYMD.length === 0) {
      this.logger.info('监控：窗口内暂无可预约日期')
      this.report({ phase: 'info', step: '暂无可约', message: '窗口内暂无可预约日期', progress: 50 })
      return
    }

    // 检查允许日期范围，筛选出需要分发预约任务的日期
    const allowed = (params.options as any)?.allowedDateRange
    const allowedDates: string[] = []
    const outOfRangeDates: string[] = []

    for (const ymd of datesYMD) {
      if (allowed && (allowed.start || allowed.end)) {
        const dTs = new Date(`${ymd}T00:00:00`).getTime()
        const sTs = parseYMD(allowed.start)
        const eTs = parseYMD(allowed.end)
        const inAllowed = (sTs === null || dTs >= sTs) && (eTs === null || dTs <= eTs)
        if (inAllowed) {
          allowedDates.push(ymd)
        }
        else {
          outOfRangeDates.push(ymd)
        }
      }
      else {
        // 没有设置允许日期范围，所有日期都可以预约
        allowedDates.push(ymd)
      }
    }

    // 记录不在允许范围内的放号
    if (outOfRangeDates.length > 0) {
      try {
        const rr = getReleaseRecordService()
        for (const d of outOfRangeDates) {
          await rr.record({
            dateYMD: d,
            office_code: monitor.office_code as any,
            service_type: monitor.service_type as any,
            reason: 'out_of_allowed_date_range',
            range_start: allowed?.start,
            range_end: allowed?.end,
            monitor_id: monitor.id,
            extra: { found_dmy: allWindowDates },
          })
        }
        this.logger.info('已记录放号但不预约（不在允许日期范围）', { 日期: outOfRangeDates, 范围: allowed, correlationId })
      }
      catch {}
      this.report({
        phase: 'info',
        step: '放号记录',
        message: `不在允许日期范围，已记录 ${outOfRangeDates.length} 天`,
        progress: 60,
        extra: { release: { timestamp: Math.floor(Date.now() / 1000), dates: outOfRangeDates, allowed, reason: 'out_of_allowed_date_range' } },
      })
    }

    // 分发预约任务：为每个候选预约和每个允许的日期创建任务
    if (allowedDates.length > 0 && candidates.length > 0) {
      const taskService = getMonitoringBookingTaskService()
      const tasksToCreate: Array<{
        monitor_id: number
        appointment_id: number
        available_date: string
        office_code: string
        service_type: string
        priority: number
        booking_mode: 'playwright' | 'request'
        booking_options: any
      }> = []

      for (const candidate of candidates) {
        for (const ymd of allowedDates) {
          tasksToCreate.push({
            monitor_id: monitor.id,
            appointment_id: candidate.id,
            available_date: ymd,
            office_code: monitor.office_code || 'HKLO',
            service_type: monitor.service_type || 'DI',
            priority: candidate.priority || 1,
            booking_mode: (params.options?.mode || 'request') as 'playwright' | 'request',
            booking_options: {
              headless: params.options?.headless ?? true,
              timeoutMs: params.options?.timeoutMs ?? 30000,
              allowedDateRange: (params.options as any)?.allowedDateRange,
              preferredDate: ymd, // 指定目标日期
            },
          })
        }
      }

      try {
        const createdTasks = await taskService.createTasks(tasksToCreate)
        this.logger.info('🎯 监控发现可预约日期，已分发预约任务', {
          日期数量: allowedDates.length,
          日期: allowedDates,
          候选数量: candidates.length,
          任务数量: createdTasks.length,
          correlationId,
        })
        this.report({
          phase: 'success',
          step: '发现可约',
          message: `发现 ${allowedDates.length} 个可预约日期，已分发 ${createdTasks.length} 个预约任务`,
          progress: 80,
          extra: {
            dates: allowedDates,
            task_count: createdTasks.length,
            candidate_count: candidates.length,
          },
        })
      }
      catch (error: any) {
        this.logger.error('分发预约任务失败', {
          error: error?.message || String(error),
          日期: allowedDates,
        })
        this.report({
          phase: 'error',
          step: '任务分发失败',
          message: error?.message || String(error),
        })
      }
    }
    else if (allowedDates.length === 0) {
      this.logger.info('监控：未发现允许范围内的可预约日期', { correlationId })
      this.report({ phase: 'info', step: '暂无可约', message: '未发现允许范围内的可预约日期', progress: 50 })
    }

    // 注意：不清理 Request 模式资源（session、flows、currentCalendarState），以便下次复用
    // 只有在 stop() 或被错误重置时才会清理
  }
}

/** 全局预约监控实例 */
let monitor: BookingAvailabilityMonitor | null = null

/**
 * 获取预约监控实例
 *
 * @description 获取全局唯一的预约监控实例，采用单例模式确保实例状态一致性。
 *
 * @returns {BookingAvailabilityMonitor} 预约监控实例
 */
export function getBookingMonitor(): BookingAvailabilityMonitor {
  if (!monitor)
    monitor = new BookingAvailabilityMonitor()
  return monitor
}
